// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Library General Public
// License as published by the Free Software Foundation; either
// version 2 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Library General Public License for more details.
// 
// You should have received a copy of the GNU Library General Public
// License along with this library; if not, write to the Free
// Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

using System;
using System.Collections.Generic;
using System.Text;
using System.Globalization;
using Yarr;


namespace Yarr.CommandConsole
{
    class ConsoleDebugger : IDebuggerBreak
    {
        Dictionary<Symbol, Boolean> BkptSymbols = new Dictionary<Symbol, Boolean>();

        public ConsoleDebugger () { }

#region IDebuggerBreak implementation

        /// <summary>
        /// Invoked by the debugger when a breakpoint is encountered
        /// </summary>
        /// <param name="debugger">The debugger instance</param>
        /// <param name="currentFrame">The current stack frame</param>
        /// <returns>The object returned replaces the actual object being evaluated</returns>
        public Object DebugBreak (IDebugger debugger, StackFrame currentFrame)
        {
            Object form = currentFrame.Form;
            if (form is Cons)
                form = DebugBreakHelper(debugger, currentFrame, form);

            return form;
        }

        /// <summary>
        /// Called when a debug message is encountered
        /// </summary>
        /// <param name="debugger">The debugger instance</param>
        /// <param name="comment">The comment to be printed</param>
        /// <param name="env">the current execution environment</param>
        public void DebugPrint (IDebugger debugger, string comment, Environment env)
        {
        }

        /// <summary>
        /// Called when a stack frame with a breakpoint is being exited
        /// </summary>
        /// <param name="debugger">The debugger instance</param>
        /// <param name="value">The value being returned</param>
        /// <param name="currentFrame">The stack frame being exited</param>
        /// <returns>The value returned replaces the actual value that was evaluated</returns>
        public Object ReturnValue (IDebugger debugger, StackFrame currentFrame, Object value)
        {
            Object form = currentFrame.Form;
            Object origValue = value;
            Object newValue = value;

            bool useDispatcher = true;
            Dictionary<Type, PrintMacro> dispatcher = 
                (Dictionary<Type, PrintMacro>)currentFrame.ExecutionEnvironment.GetValue("system", "*pprint-dispatch*");
            if (dispatcher == null)
                useDispatcher = false;

            if (value is System.Exception)
                Console.WriteLine("***** Exception *****\n{0}\n***** Exception *****", ((Exception)value).Message);

            do
            {
                value = newValue;
                if (form is Cons)
                {
                    if (useDispatcher)
                    {
                        PrintState result = new PrintState(Console.BufferWidth, 2, dispatcher);

                        result.Space(currentFrame.StackDepth);
                        result.Write(String.Format(CultureInfo.InvariantCulture, "{0} << ", currentFrame.StackDepth));
                        result.PushRelativeIndent(1);
                        PrettyPrinter.PprintDispatch(value, result, currentFrame.ExecutionEnvironment);
                        Console.WriteLine(result.ToString());
                    }
                    else
                    {
                        Indent(currentFrame.StackDepth);
                        Console.WriteLine("{0} << {1}", currentFrame.StackDepth, value);
                    }
                    newValue = ProcessDebuggerCommands(debugger, currentFrame, value);
                }
                else if (form != value)
                {
                    if (useDispatcher)
                    {
                        PrintState result = new PrintState(Console.BufferWidth, 2, dispatcher);

                        result.Space(currentFrame.StackDepth);
                        result.Write(String.Format(CultureInfo.InvariantCulture, "{0} << {1} := ", currentFrame.StackDepth,
                                                                    (value == origValue) ? currentFrame.Form : "*"));
                        result.PushRelativeIndent(1);
                        PrettyPrinter.PprintDispatch(value, result, currentFrame.ExecutionEnvironment);
                        Console.WriteLine(result.ToString());
                    }
                    else
                    {
                        Indent(currentFrame.StackDepth);
                        Console.WriteLine("{0} << {1} := {2}", currentFrame.StackDepth,
                                            (value == origValue) ? currentFrame.Form : "*", value);
                        newValue = ProcessDebuggerCommands(debugger, currentFrame, value);
                    }
                }

                if (newValue != value)
                    newValue = Runtime.Eval(newValue, currentFrame.ExecutionEnvironment);

            } while (value != newValue);

            if (currentFrame.StackDepth == 0)
                Console.WriteLine();

            return newValue;
        }

#endregion

        private void Indent (int spaces)
        {
            Console.Write(String.Format(CultureInfo.InvariantCulture, "{0," + spaces.ToString() + "}", " "));
        }

        private Object DebugBreakHelper (IDebugger debugger, StackFrame currentFrame, Object form)
        {

            bool useDispatcher = true;
            Dictionary<Type, PrintMacro> dispatcher = 
                (Dictionary<Type, PrintMacro>)currentFrame.ExecutionEnvironment.GetValue("system", "*pprint-dispatch*");
            if (dispatcher == null)
                useDispatcher = false;

            Object newForm = form;
            do
            {
                if (useDispatcher)
                {
                    PrintState result = new PrintState(Console.BufferWidth, 2, dispatcher);

                    result.Space(currentFrame.StackDepth);
                    result.Write(String.Format(CultureInfo.InvariantCulture, "{0} >> ", currentFrame.StackDepth));
                    result.PushRelativeIndent(1);
                    PrettyPrinter.PprintDispatch(form, result, currentFrame.ExecutionEnvironment);
                    Console.WriteLine(result.ToString());
                }
                else
                {
                    Indent(currentFrame.StackDepth);
                    Console.WriteLine("{0} >> {1}", currentFrame.StackDepth, form);
                }

                newForm = ProcessDebuggerCommands(debugger, currentFrame, form);

            } while (newForm != form);

            return newForm;
        }

        private Object ProcessDebuggerCommands (IDebugger debugger, StackFrame frame, Object value)
        {
            bool done = false;
            string input;
            int arg;
            StackFrame[] stack = null;

            while (!done)
            {
                if (stack == null  ||  frame.StackDepth >= stack[0].StackDepth)
                    Console.Write(":");
                else
                    Console.Write("{0}:", frame.StackDepth);

                switch (Console.ReadKey(true).Key)
                {
                    //Step into
                    case ConsoleKey.DownArrow:
                    case ConsoleKey.S:
                    case ConsoleKey.F11:
                        debugger.SetMode(DebuggerMode.SingleStep, frame, -1);
                        done = true;
                        break;

                    // Set stack frame
                    case ConsoleKey.F:
                        if (stack == null)
                            stack = debugger.GetCallstack();
                        Console.Write("Frame> ");
                        while (String.IsNullOrEmpty(input = Console.ReadLine()))
                            ;
                        if (Int32.TryParse(input, out arg))
                        {
                            if (arg >= 0 && arg < stack.Length)
                                frame = stack[stack.Length - arg - 1];
                        }
                        break;

                    // Step over
                    case ConsoleKey.RightArrow:
                    case ConsoleKey.N:
                    case ConsoleKey.F10:
                        debugger.SetMode(DebuggerMode.StepOver, frame, -1);
                        done = true;
                        break;

                    // Return from current form
                    case ConsoleKey.UpArrow:
                    case ConsoleKey.U:
                        debugger.SetMode(DebuggerMode.RunToPreviousFrame, frame, 1);
                        done = true;
                        break;

                    // Run to next explicit bkpt
                    case ConsoleKey.Tab:
                    case ConsoleKey.F5:
                        debugger.SetMode(DebuggerMode.RunToBreakpoint, frame, -1);
                        Console.WriteLine();
                        done = true;
                        break;

                    // Run to completion
                    case ConsoleKey.G:
                        Console.WriteLine();
                        debugger.SetMode(DebuggerMode.RunToCompletion, frame, -1);
                        done = true;
                        break;

                    // Print breakpoints
                    case ConsoleKey.B:
                        PrintBkpts();
                        break;

                    // Print call stack
                    case ConsoleKey.C:
                    case ConsoleKey.Oem2:
                        if (stack == null)
                            stack = debugger.GetCallstack();
                        PrintCallStack(stack);
                        break;

                    // Evaluate in current environment
                    case ConsoleKey.E:
                    case ConsoleKey.Spacebar:
                    case ConsoleKey.OemPeriod:
                        Console.Write("Eval> ");
                        while (String.IsNullOrEmpty(input = Console.ReadLine()))
                            ;
                        Console.WriteLine(":= {0}", Runtime.EvalString(input, 
                                                                      frame.ExecutionEnvironment));
                        break;

                    // Set return value
                    case ConsoleKey.LeftArrow:
                    case ConsoleKey.R:
                        Console.Write("Return> ");
                        value = Runtime.EvalString("(read)", frame.ExecutionEnvironment);
                        done = true;
                        break;

                    // Set breakpoint
                    case ConsoleKey.Insert:
                    case ConsoleKey.NumPad0:
                    case ConsoleKey.Add:
                    case ConsoleKey.OemPlus:
                        Console.Write("Set Bkpt> ");
                        AddBreakpoint(Console.ReadLine(), debugger, frame);
                        break;

                    // Remove breakpoint
                    case ConsoleKey.Delete:
                    case ConsoleKey.Decimal:
                    case ConsoleKey.Subtract:
                    case ConsoleKey.OemMinus:
                        Console.Write("Unset Bkpt> ");
                        RemoveBreakpoint(Console.ReadLine(), debugger, frame);
                        break;

                    // Toggle breakpoint
                    case ConsoleKey.Multiply:
                    case ConsoleKey.D8:
                    case ConsoleKey.T:
                        Console.Write("Toggle Bkpt> ");
                        ToggleBreakpoint(Console.ReadLine(), debugger, frame);
                        break;

                    // Quit / Abort
                    case ConsoleKey.Escape:
                    case ConsoleKey.Q:
                        Console.WriteLine();
                        debugger.SetMode(DebuggerMode.Abort, frame, -1);
                        done = true;
                        break;

                    default:
                        DebugHelp();
                        break;
                }
            }

            return value;
        }

        private void DebugHelp ()
        {
            Console.WriteLine("\n***** Debugger Commands *****");
            Console.WriteLine(" DownArrow  or F11 : Step into  |  + or insert : Add Bkpt");
            Console.WriteLine("RighttArrow or F10 : Step over  |  - or delete : Remove Bkpt");
            Console.WriteLine("     Tab or F5     : Continue   |     * or T   : Toggle Bkpt");
            Console.WriteLine("   UpArrow or U    : Return     |       B      : Print breakpoints");
            Console.WriteLine("  LeftArrow or R   : Set current form/value");
            Console.WriteLine("    E or Space     : Evaluate expression");
            Console.WriteLine("      C or ?       : Print Call Stack");
            Console.WriteLine("        F          : Set Stack Frame");
            Console.WriteLine("   Escapce or Q    : Quit (halt execution)");
            Console.WriteLine("        G          : Quit (continue execution)");
            Console.WriteLine("***** Debugger Commands *****");
        }

        private void PrintCallStack (StackFrame[] callStack)
        {
            Console.WriteLine("\n***** Top of Stack *****");
            for (int ix = 0; ix < callStack.Length; ++ix)
            {
                string elt = callStack[ix].Form.ToString();
                if (elt.Length > 60)
                    elt = elt.Substring(0, 60) + " ... ";
                Console.WriteLine("[{0,4}] {1}", callStack[ix].StackDepth, elt);
            }
            Console.WriteLine("****** Call Stack ******");
        }

        private void AddBreakpoint (string bkpt, IDebugger debugger, StackFrame frame)
        {
            while (String.IsNullOrEmpty(bkpt))
                bkpt = Console.ReadLine();

            bkpt = bkpt.Trim();
            if (String.IsNullOrEmpty(bkpt))
                return;

            Symbol bkptName = Symbol.FromName(bkpt);
            Object bkptObj = Runtime.EvalString(bkpt, frame.ExecutionEnvironment);

            debugger.AddBreakpoint(bkptObj);
            BkptSymbols[bkptName] = true;

            PrintBkpts();
        }

        private void RemoveBreakpoint (string bkpt, IDebugger debugger, StackFrame frame)
        {
            while (String.IsNullOrEmpty(bkpt))
                bkpt = Console.ReadLine();

            bkpt = bkpt.Trim();
            if (String.IsNullOrEmpty(bkpt))
                return;

            Symbol bkptName = Symbol.FromName(bkpt);
            Object bkptObj = Runtime.EvalString(bkpt, frame.ExecutionEnvironment);

            debugger.RemoveBreakpoint(bkptObj);
            if (BkptSymbols.ContainsKey(bkptName))
                BkptSymbols.Remove(bkptName);

            PrintBkpts();
        }

        private void ToggleBreakpoint (string bkpt, IDebugger debugger, StackFrame frame)
        {
            while (String.IsNullOrEmpty(bkpt))
                bkpt = Console.ReadLine();

            bkpt = bkpt.Trim();
            if (String.IsNullOrEmpty(bkpt))
                return;

            Symbol bkptName = Symbol.FromName(bkpt);
            Object bkptObj = Runtime.EvalString(bkpt, frame.ExecutionEnvironment);

            if (BkptSymbols.ContainsKey(bkptName))
            {
                bool isActive;
                BkptSymbols[bkptName] = isActive = !BkptSymbols[bkptName];
                if (isActive)
                    debugger.EnableBreakpoint(bkptObj);
                else
                    debugger.DisableBreakpoint(bkptObj);

            }

            PrintBkpts();
        }

        private void PrintBkpts ()
        {
            StringBuilder sbActive = new StringBuilder();
            StringBuilder sbInactive = new StringBuilder();

            foreach (Symbol key in BkptSymbols.Keys)
            {
                if (BkptSymbols[key])
                {
                    sbActive.Append(key.Name);
                    sbActive.Append(" ");
                }
                else
                {
                    sbInactive.Append(key.Name);
                    sbInactive.Append(" ");
                }
            }

            string strActive = sbActive.ToString();
            string strInactive = sbInactive.ToString();

            Console.WriteLine("\n***** Breakpoints *****");
            if (!String.IsNullOrEmpty(strActive))
                Console.WriteLine("  Active: {0}", strActive);
            if (!String.IsNullOrEmpty(strInactive))
                Console.WriteLine("Inactive: {0}", strInactive);
            Console.WriteLine("***** Breakpoints *****");
        }
    }
}
